/*******************************************************************************
 * Copyright (C) 2010 Chris Wroe
 * 
 * Modifications to the initial code base are copyright of their respective
 * authors, or their employers as appropriate.
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307
 ******************************************************************************/

package uk.co.wroedevelopments.semanticworkbench.store;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.neo4j.api.core.Direction;
import org.neo4j.api.core.NeoService;
import org.neo4j.api.core.Node;
import org.neo4j.api.core.Relationship;
import org.neo4j.api.core.RelationshipType;
import org.neo4j.api.core.ReturnableEvaluator;
import org.neo4j.api.core.StopEvaluator;
import org.neo4j.api.core.Transaction;
import org.neo4j.api.core.TraversalPosition;
import org.neo4j.api.core.Traverser;
import org.neo4j.api.core.Traverser.Order;
import org.neo4j.util.index.LuceneIndexService;
import org.xtext.example.semDsl.Attribute;
import org.xtext.example.semDsl.AttributeDefinition;
import org.xtext.example.semDsl.Concept;
import org.xtext.example.semDsl.Name;
import org.xtext.example.semDsl.Restriction;

import com.sun.tools.javac.util.Position;

public class KnowledgeBaseStore implements IKnowledgeBase {
//TODO refactor much of the code here into concept model adaptors and axiom object model adaptors
	public static final String STATED = "Stated";

	private NeoService connection;
	private LuceneIndexService index;
	private List<OntologyChangeListener> ontologyChangeListeners;

	public KnowledgeBaseStore(NeoService connection, LuceneIndexService index) {
		this.connection = connection;
		this.index = index;
		this.ontologyChangeListeners = new ArrayList<OntologyChangeListener>();
	}

	public void addOntologyChangeListener(OntologyChangeListener listener)
			throws Exception {
		if (!ontologyChangeListeners.contains(listener)) {
			ontologyChangeListeners.add(listener);
		} else {
			throw new Exception(
					"Attempt to add a listener that has already been added");
		}
	}

	public void removeOntologyChangeListener(OntologyChangeListener listener)
			throws Exception {
		if (ontologyChangeListeners.contains(listener)) {
			ontologyChangeListeners.remove(listener);
		} else {
			throw new Exception(
					"Attempt to remove a listener that has not been added");
		}
	}

	public Node addConcept(Concept c) {

		Node result = null;
		Transaction tx = connection.beginTx();
		try {
			result = connection.createNode();
			result.setProperty(NodePropertyType.NAME.name(), c.getName());
			index.index(result, NodePropertyType.NAME.name(), c.getName());
			tx.success();

		} finally {
			tx.finish();
		}
		conceptAdded(c, result);
		return result;

	}

	private void conceptAdded(Concept c, Node result) {
		for (OntologyChangeListener l : ontologyChangeListeners) {
			l.conceptAdded(c, result);
		}

	}

	public Node addConceptInclusionAxiom(Node nodeA, Node nodeB) {
		Node axiomNode = null;
		Transaction tx = connection.beginTx();
		try {
			axiomNode = connection.createNode();
			axiomNode.setProperty(NodePropertyType.NAME.name(),
					"Concecpt Inclusion Axiom " + getName(nodeA) + " [ "
							+ getName(nodeB));
			// create or get a subsumption relationship between A and B

			hookAxiomToNewOrExistingSubsumptionRelationship(nodeA, nodeB,
					axiomNode);

			// finally hook up the axiom node to a root node
			Node topAxiomNode = getConceptNamed(AxiomType.CONCEPT_INCLUSION
					.name());
			axiomNode.createRelationshipTo(topAxiomNode,
					SemanticRelationshipType.INSTANCE_OF);

			tx.success();

		} finally {
			tx.finish();
		}

		return axiomNode;

	}

	private void hookAxiomToNewOrExistingSubsumptionRelationship(Node nodeA,
			Node nodeB, Node axiomNode) {
		Relationship rel = nodeA.getSingleRelationship(
				SemanticRelationshipType.SUBSUMPTION, Direction.OUTGOING);
		if (rel == null) {
			// create a relationship
			rel = nodeA.createRelationshipTo(nodeB,
					SemanticRelationshipType.SUBSUMPTION);
			// FIXME this stated may need pulling out if completion rules use
			// this method
			rel.setProperty(RelationshipPropertyType.STATED.name(), true);
		}
		// use an array of long node ids to link the relationship back to the
		// axiom that created it
		long[] axioms;
		if (!rel.hasProperty(NodePropertyType.AXIOM.name())) {
			axioms = new long[0];
		} else {
			axioms = (long[]) rel.getProperty(NodePropertyType.AXIOM.name());
		}
		// grow the array
		long[] newAxioms = Arrays.copyOf(axioms, axioms.length + 1);
		// add the reference to the axiom node
		newAxioms[newAxioms.length - 1] = axiomNode.getId();
		// set the property
		rel.setProperty(NodePropertyType.AXIOM.name(), newAxioms);

		// now add the opposite from axiom node to relationship
		// assume an axiom only implies one relationship for now

		axiomNode
				.setProperty(NodePropertyType.RELATIONSHIP.name(), rel.getId());
	}

	public Node addConjunctionAxiom(Node conjunctionNode, Node nodeA, Node nodeB) {

		// Now add the one going to other way

		// add ????????????????
		// create an axiom
		// create a relationship to conjunction node (C)

		// create a relationship to A
		// create a relationship to B
		Node axiomNode = null;
		Transaction tx = connection.beginTx();
		try {
			axiomNode = connection.createNode();
			axiomNode.setProperty(NodePropertyType.NAME.name(), 
					"Conjunction Axiom  " + getName(nodeA) + " ^ " + getName(nodeB) + " [ " + getName(conjunctionNode));
			// create or get a subsumption relationship between A and B

			axiomNode.createRelationshipTo(conjunctionNode,
					SemanticRelationshipType.CONJUNCTION);

			// finally hook up the axiom node to a root node
			Node topAxiomNode = getConceptNamed(AxiomType.CONJUNCTION
					.name());
			axiomNode.createRelationshipTo(topAxiomNode,
					SemanticRelationshipType.INSTANCE_OF);

			tx.success();

		} finally {
			tx.finish();
		}

		return axiomNode;

	}

	private String getName(Node node) {
		return (String) node.getProperty(NodePropertyType.NAME.name());
	}

	public Node addNegativeExistentialAxiom(Node nodeS, Node nodeB,
			Node restrictionNode) {
		// TODO Auto-generated method stub
		Node axiomNode = null;
		Transaction tx = connection.beginTx();
		try {
			axiomNode = addRestrictionAxiom(restrictionNode,
					AxiomType.EXISTENTIAL_NEGATIVE);
			axiomNode.setProperty(NodePropertyType.NAME.name(),
					"Negative Existential Axiom  " + getName(restrictionNode)
							+ " [ " + restrictionNode.getId());
			tx.success();
		} finally {
			tx.finish();
		}
		return axiomNode;
	}

	public Node addPositiveExistentialAxiom(Node restrictionNode, Node nodeR,
			Node nodeB) {
		// what to do here
		// add a subsumption between node A and restriction nodeR, nodeB
		Node axiomNode = null;
		Transaction tx = connection.beginTx();
		try {
			axiomNode = addRestrictionAxiom(restrictionNode,
					AxiomType.EXISTENTIAL_POSITIVE);
			axiomNode.setProperty(NodePropertyType.NAME.name(),
					"Positive Existential Axiom  " + restrictionNode.getId()
							+ " [ " + getName(restrictionNode));
			tx.success();
		} finally {
			tx.finish();
		}
		return axiomNode;
	}

	private Node addRestrictionAxiom(Node restrictionNode,
			AxiomType axiomType) {
		Node axiomNode = null;

		axiomNode = connection.createNode();

		// create or get a subsumption relationship between A and B

		axiomNode.createRelationshipTo(restrictionNode,
				SemanticRelationshipType.RESTRICTION);

		// finally hook up the axiom node to a root node
		Node topAxiomNode = getConceptNamed(axiomType.name());
		axiomNode.createRelationshipTo(topAxiomNode,
				SemanticRelationshipType.INSTANCE_OF);

		

		return axiomNode;
	}

	public Node addRoleInclusionAxiom(Node nodeR, Node nodeS) {
		Node axiomNode = null;
		Transaction tx = connection.beginTx();
		try {
			axiomNode = connection.createNode();
			axiomNode.setProperty(NodePropertyType.NAME.name(),
					"Role Inclusion Axiom " + getName(nodeR) + " [ "
							+ getName(nodeS));
			// create or get a subsumption relationship between A and B
			hookAxiomToNewOrExistingSubsumptionRelationship(nodeR, nodeS,
					axiomNode);

			// finally hook up the axiom node to a root node
			Node topAxiomNode = getConceptNamed(AxiomType.ROLE_INCLUSION
					.name());
			axiomNode.createRelationshipTo(topAxiomNode,
					SemanticRelationshipType.INSTANCE_OF);

			tx.success();

		} finally {
			tx.finish();
		}

		return axiomNode;

	}

	
	private Node getConceptNode(Concept c) {
		Node result = null;
		Transaction tx = connection.beginTx();
		try {
			result = index.getSingleNode(NodePropertyType.NAME.name(), c
					.getName());
			tx.success();
		} finally {
			tx.finish();
		}
		return result;

	}

	private void newSubConceptAdded(Node childNode, Node parentNode) {
		
			for (OntologyChangeListener l : ontologyChangeListeners) {
				l.newSubConceptAdded(childNode, parentNode);
			}
	
		
	}

	public Node addNewSub(Concept parentConcept, Concept c) {
		Node childNode = addConcept(c);
		Node parentNode = getConceptNode(parentConcept);
		//addSubsumption(parentConcept, c, Boolean.TRUE);
		newSubConceptAdded(childNode,parentNode);
		return childNode;
	
	}

	public Node addTopLevelConcept(Concept c) {
		Node childNode = addConcept(c);
		Node parentNode = getTopConceptNode();
		
		newSubConceptAdded(childNode, parentNode);
		return childNode;
	}

	private Node getTopConceptNode() {
		return getConceptNamed(NodeConstants.TOP_CONCEPT.name());
	}

	private Node getConceptInclusionAxiomClassNode() {
		return getConceptNamed(AxiomType.CONCEPT_INCLUSION.name());
	}

	private Node getConjunctionAxiomClassNode() {
		return getConceptNamed(AxiomType.CONJUNCTION.name());
	}

	private Node getRoleInclusionAxiomClassNode() {
		return getConceptNamed(AxiomType.ROLE_INCLUSION.name());
	}

	private Node getPositiveExistentialAxiomClassNode() {
		return getConceptNamed(AxiomType.EXISTENTIAL_POSITIVE
				.name());
	}

	private Node getNegativeExistentialAxiomClassNode() {
		return getConceptNamed(AxiomType.EXISTENTIAL_NEGATIVE
				.name());
	}

	public void addAttribute(AttributeDefinition def, Attribute parent) {
		Node attNode = addAttribute(def);
		Node parentNode= null;
		
		Transaction tx = connection.beginTx();
		
		try {
			parentNode = getAttributeNode(parent);
			addRoleInclusionAxiom(attNode, parentNode);

			tx.success();
		} finally {
			tx.finish();
		}
		attributeAdded(attNode,parentNode);
	}

	private void attributeAdded(Node attNode, Node parentNode) {
		for (OntologyChangeListener l : ontologyChangeListeners) {
			l.attributeAdded(attNode,parentNode);
		}
		
	}

	private Node getAttributeNode(Attribute attribute) {
		Node result = null;
		Transaction tx = connection.beginTx();
		try {
			result = index.getSingleNode("name", attribute.getName());
			tx.success();
		} finally {
			tx.finish();
		}
		return result;
	}

	private Node addAttribute(AttributeDefinition def) {
		// FIXME inverse attribute needs adding
		Node result = null;
		Transaction tx = connection.beginTx();
		try {
			result = connection.createNode();
			result.setProperty(NodePropertyType.NAME.name(), def.getAttribute().getName());
			result.setProperty(NodePropertyType.ATTRIBUTE_CARDINALITY.name(),
					def.getAttributeCardinality().getValue());
			result.setProperty(NodePropertyType.ATTRIBUTE_SCOPE.name(), def
					.getAttributeScope().getValue());
			index.index(result, NodePropertyType.NAME.name(), def.getAttribute().getName());
			tx.success();

		} finally {
			tx.finish();
		}
		return result;
	}

	public void addTopLevelAttribute(AttributeDefinition def) {
		Node attNode = addAttribute(def);
		Node parentNode=null;
		Transaction tx = connection.beginTx();
		
		try {
			parentNode = getTopAttributeNode();
			attNode.createRelationshipTo(parentNode,
					SemanticRelationshipType.SUBSUMPTION);
			
			tx.success();
		} finally {
			tx.finish();
		}
		attributeAdded(attNode,parentNode);
	}

	private Node getTopAttributeNode() {
		return getConceptNamed(NodeConstants.TOP_ATTRIBUTE.name());

	}

	public Node getConceptNamed(String name) {
		Node result = null;
		Transaction tx = connection.beginTx();
		try {
			result = index.getSingleNode(NodePropertyType.NAME.name(), name);
			tx.success();
		} finally {
			tx.finish();
		}
		return result;
	}

	public Node addConceptDefinition(Name nameExpression) {
		Node result = null;
		boolean positive = true;
		boolean negative = true;
		// steps
		// 1 get hold of supertype
		// need to build up a conjunction and flag it as + and -
		// what to do about multiple conjunction? Use a LISP list?
		Concept superkind = nameExpression.getFrame().getSuperKind();
		Node superkindNode = getConceptNode(superkind);

		// 2 get hold of restriction list
		List<Restriction> restrictions = nameExpression.getFrame()
				.getRestriction();

		// 3 add Node for new Concept
		Concept newConcept = nameExpression.getConcept();
		Node newConceptNode = addConcept(newConcept);

		// add node for restrictions
		List<Node> restrictionNodes = new ArrayList<Node>();
		for (Restriction r : restrictions) {
			// how do you know whether an existing restriction node exists
			// have to expand into normal form first??
			// add a restriction node
			restrictionNodes.add(getOrAddRestriction(r, positive, negative));
			// not yet addSubsumption(restricionNode, newConceptNode,
			// Boolean.TRUE);

		}

		// 4 add conjunctions via a relationship
		// need to build up conjunctions from simpler pairs
		Node restrictionRootNode = null;
		if (restrictionNodes.size() > 1) {
			restrictionRootNode = createConjunctionTree(restrictionNodes,
					positive, negative, superkindNode);
		} else {// or else just take the restriction
			restrictionRootNode = restrictionNodes.get(0);
		} // then add a conjunction node for the super concept plus restriction
		addConjunctionUsingExistingNode(newConceptNode, superkindNode,
				restrictionRootNode, positive, negative, superkindNode);

	

		// add axioms
		// C [ D
		// nill

		return result;

	}

	private Node addConjunction(Node nodeA, Node nodeB, boolean positive,
			boolean negative, Node superkindNode) {
		return addConjunctionUsingExistingNode(null, nodeA, nodeB, positive,
				negative, superkindNode);

	}

	// FIXME normalisation happening at the same time as addition
	// with use of superkind node
	private Node createConjunctionTree(List<Node> restrictionNodes,
			boolean positive, boolean negative, Node superkindNode) {
		int index = restrictionNodes.size() - 1;

		Node nodeB = restrictionNodes.get(index);
		index = index - 1;
		Node nodeA = restrictionNodes.get(index);
		Node conjunction = addConjunction(nodeA, nodeB, positive, negative,
				superkindNode);
		// pass that conjunction to the build up the next one
		// repeat until at the first restriction
		while (index > 0) {
			index = index - 1;
			nodeA = restrictionNodes.get(index);
			conjunction = addConjunction(nodeA, conjunction, positive,
					negative, superkindNode);
		}
		return conjunction;
	}

	Node addConjunctionUsingExistingNode(Node existingNode, Node nodeA,
			Node nodeB, boolean positive, boolean negative, Node superkindNode) {

		Transaction tx = connection.beginTx();
		try {

			// but how, assume nodes are already present
			// we already have the nodes.
			// need to create a new one and then add relationships to it
			if (existingNode == null) {
				existingNode = connection.createNode();
			}
			existingNode
					.setProperty(NodePropertyType.POSITIVE.name(), positive);
			existingNode
					.setProperty(NodePropertyType.NEGATIVE.name(), negative);

			existingNode.createRelationshipTo(nodeA,
					SemanticRelationshipType.AND);
			existingNode.createRelationshipTo(nodeB,
					SemanticRelationshipType.AND);
			tx.success();

		} finally {
			tx.finish();
		}
		conjunctionAdded(existingNode, nodeA, nodeB, positive, negative,
				superkindNode);
		return existingNode;
	}

	private void conjunctionAdded(Node conjunctionNode, Node nodeA, Node nodeB,
			boolean positive, boolean negative, Node superkindNode) {
		for (OntologyChangeListener l : ontologyChangeListeners) {
			l.conjunctionAdded(conjunctionNode, nodeA, nodeB, negative,
					positive, superkindNode);
		}

	}

	public Node getOrAddRestriction(Restriction r, boolean positive,
			boolean negative) {
		// TODO Auto-generated method stub
		Node restrictionNode = null;
		Node attributeNode = null;
		Node valueNode = null;
		String restrictionName = getRetrictionName(r);
		Transaction tx = connection.beginTx();
		try {
			restrictionNode = index.getSingleNode(NodePropertyType.NAME.name(),
					restrictionName);
			if (restrictionNode == null) { // no restriction yet
				restrictionNode = connection.createNode();
				restrictionNode.setProperty(NodePropertyType.NAME.name(),
						restrictionName);
				index.index(restrictionNode, NodePropertyType.NAME.name(),
						restrictionName);

				attributeNode = getAttributeNode(r.getAttribute());
				valueNode = getConceptNode(r.getValue());
				restrictionNode.createRelationshipTo(attributeNode,
						SemanticRelationshipType.ATTRIBUTE);
				restrictionNode.createRelationshipTo(valueNode,
						SemanticRelationshipType.VALUE);
				Node topRestriction = getTopRestriction();
				restrictionNode.createRelationshipTo(topRestriction,
						SemanticRelationshipType.SUBSUMPTION);

			}
			// update positive negative status
			// TODO not sure about this positive negative thing, needs reviewing
			restrictionNode.setProperty(NodePropertyType.POSITIVE.name(),
					positive);
			restrictionNode.setProperty(NodePropertyType.NEGATIVE.name(),
					negative);

			tx.success();

		} finally {
			tx.finish();
		}
		restrictionAdded(restrictionNode, attributeNode, valueNode, positive,
				negative);
		return restrictionNode;
	}

	private Node getTopRestriction() {
		return getConceptNamed(NodeConstants.TOP_RESTRICTION.name());
	}

	private void restrictionAdded(Node restrictionNode, Node attributeNode,
			Node valueNode, boolean positive, boolean negative) {
		for (OntologyChangeListener l : ontologyChangeListeners) {
			l.restrictionAdded(restrictionNode, attributeNode, valueNode,
					positive, negative);
		}

	}

	private String getRetrictionName(Restriction r) {
		StringBuffer result = new StringBuffer();
		result.append(r.getAttribute().getName());
		result.append(" ");
		result.append(r.getValue().getName());
		return result.toString();
	}

	public Traverser getAllAxiomOfType(AxiomType axiomType) {
		Node rootNode = getConceptNamed(axiomType.name());
		Traverser traverser = rootNode.traverse(Order.BREADTH_FIRST, StopEvaluator.DEPTH_ONE, ReturnableEvaluator.ALL_BUT_START_NODE,
				SemanticRelationshipType.INSTANCE_OF, Direction.INCOMING);
		return traverser;
	}
	
	public Traverser treeWalk(Node topNode) {
		Traverser traverser = topNode.traverse(Order.BREADTH_FIRST, StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL,SemanticRelationshipType.SUBSUMPTION,Direction.INCOMING);
		return traverser;
	}

	public boolean subsumedBy(Node value, Node testValue) {
		boolean subsumedBy=false;
		Iterator<Node> iterator = value.traverse(Order.BREADTH_FIRST, StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL_BUT_START_NODE, SemanticRelationshipType.SUBSUMPTION,Direction.OUTGOING).iterator();
		while (iterator.hasNext() && ! subsumedBy) {
			Node nextNode = iterator.next();
			if (nextNode.equals(testValue)) {
				subsumedBy=true;
			}
		}
		return subsumedBy;
	}

	public String getNodeName(Node value) {
		String name = "un-named";
		if (value.hasProperty(NodePropertyType.NAME.name())) {
			name = (String) value.getProperty(NodePropertyType.NAME.name());
		}
		return name;
	}
	
}
